Interactive Visualisation and Dashboards#
import pypsa
import atlite
import pandas as pd
import geopandas as gpd
import xarray as xr
import matplotlib.pyplot as plt
plt.style.use("bmh")
Show code cell content
from urllib.request import urlretrieve
from os.path import basename
urls = [
"https://tubcloud.tu-berlin.de/s/2oogpgBfM5n4ssZ/download/PORTUGAL-2013-01-era5.nc",
]
for url in urls:
urlretrieve(url, basename(url))
Load Example Data#
First, let’s load a few example datasets you know from previous tutorials.
A PyPSA network:
n = pypsa.Network(
"https://tubcloud.tu-berlin.de/s/kpWaraGc9LeaxLK/download/network-cem.nc"
)
INFO:pypsa.io:Retrieving network data from https://tubcloud.tu-berlin.de/s/kpWaraGc9LeaxLK/download/network-cem.nc
WARNING:pypsa.io:Importing network from PyPSA version v0.21.3 while current version is v0.25.1. Read the release notes at https://pypsa.readthedocs.io/en/latest/release_notes.html to prepare your network for import.
INFO:pypsa.io:Imported network network-cem.nc has buses, carriers, generators, global_constraints, loads, storage_units
n.optimize(solver_name="cbc");
Show code cell output
INFO:linopy.model: Solve problem using Cbc solver
INFO:linopy.io:Writing objective.
Writing constraints.: 0%| | 0/15 [00:00<?, ?it/s]
Writing constraints.: 40%|████ | 6/15 [00:00<00:00, 51.87it/s]
Writing constraints.: 80%|████████ | 12/15 [00:00<00:00, 43.98it/s]
Writing constraints.: 100%|██████████| 15/15 [00:00<00:00, 40.89it/s]
Writing continuous variables.: 0%| | 0/7 [00:00<?, ?it/s]
Writing continuous variables.: 100%|██████████| 7/7 [00:00<00:00, 114.59it/s]
INFO:linopy.io: Writing time: 0.46s
Welcome to the CBC MILP Solver
Version: 2.10.10
Build Date: Apr 19 2023
command line - cbc -printingOptions all -import /tmp/linopy-problem-zije53jv.lp -solve -solu /tmp/linopy-solve-hcezwibh.sol (default strategy 1)
Option for printingOptions changed from normal to all
Presolve 25230 (-25147) rows, 18665 (-3241) columns and 69120 (-32766) elements
Perturbing problem by 0.001% of 232.80852 - largest nonzero change 0.0003084482 ( 7.1472055%) - largest zero change 0.00030841019
0 Obj 0 Primal inf 1.7299319e+08 (2190)
351 Obj 1485.0331 Primal inf 2.0215782e+08 (2517)
702 Obj 19742.419 Primal inf 2.3731013e+08 (2833)
1053 Obj 1.1540031e+10 Primal inf 2.1548549e+08 (2266)
1404 Obj 1.7500237e+10 Primal inf 2.4173489e+08 (3141)
1755 Obj 1.7500464e+10 Primal inf 2.714411e+08 (3128)
2106 Obj 1.7503766e+10 Primal inf 4.7081187e+08 (3183)
2457 Obj 1.7504025e+10 Primal inf 3.9448973e+08 (3277)
2808 Obj 1.7504176e+10 Primal inf 4.4155116e+08 (3342)
3159 Obj 1.7504243e+10 Primal inf 4.4590873e+08 (3396)
3510 Obj 1.7504385e+10 Primal inf 4.9981172e+08 (3386)
3861 Obj 1.7504943e+10 Primal inf 3.7653676e+08 (3472)
4212 Obj 1.7510475e+10 Primal inf 4.2947079e+08 (3465)
4563 Obj 1.7537229e+10 Primal inf 5.0847506e+08 (3517)
4914 Obj 1.7537361e+10 Primal inf 3.81969e+08 (3581)
5265 Obj 1.7537545e+10 Primal inf 4.4613797e+08 (3689)
5616 Obj 1.7537743e+10 Primal inf 5.9105401e+08 (3834)
5967 Obj 1.7538618e+10 Primal inf 1.0579895e+09 (3781)
6318 Obj 1.7539497e+10 Primal inf 8.6213683e+08 (3891)
6669 Obj 2.279583e+10 Primal inf 6.8967082e+08 (3650)
7020 Obj 2.2815623e+10 Primal inf 7.5849888e+08 (3681)
7371 Obj 2.2980695e+10 Primal inf 1.53387e+09 (3688)
7722 Obj 4.0709544e+10 Primal inf 4.8252512e+08 (3699)
8073 Obj 4.0948369e+10 Primal inf 5.5175944e+08 (3799)
8424 Obj 4.3244699e+10 Primal inf 4.3671926e+08 (3766)
8775 Obj 4.3245576e+10 Primal inf 4.6116642e+08 (3841)
9126 Obj 5.0580272e+10 Primal inf 7.7524645e+08 (3759)
9477 Obj 5.4329343e+10 Primal inf 6.1613508e+08 (4944)
9828 Obj 5.4340236e+10 Primal inf 5.1537472e+08 (4753)
10179 Obj 5.4341025e+10 Primal inf 5.7721788e+08 (4804)
10530 Obj 5.4341176e+10 Primal inf 4.4177501e+08 (4810)
10881 Obj 5.4341328e+10 Primal inf 4.7698641e+08 (4807)
11232 Obj 5.4343627e+10 Primal inf 5.2815719e+08 (4813)
11583 Obj 5.4343776e+10 Primal inf 4.9550684e+08 (4884)
11934 Obj 5.4358508e+10 Primal inf 5.7296022e+08 (4886)
12285 Obj 5.4358799e+10 Primal inf 5.2357161e+08 (4886)
12636 Obj 5.4373364e+10 Primal inf 7.2943238e+08 (4935)
12987 Obj 5.4379354e+10 Primal inf 6.9146985e+08 (4905)
13338 Obj 5.4379571e+10 Primal inf 1.0893155e+09 (4942)
13689 Obj 5.4379719e+10 Primal inf 8.2290461e+08 (4961)
14040 Obj 5.4380208e+10 Primal inf 7.1814611e+08 (4965)
14391 Obj 5.4380646e+10 Primal inf 6.1765819e+08 (4982)
14742 Obj 5.4608903e+10 Primal inf 5.6701623e+08 (4814)
15093 Obj 5.463169e+10 Primal inf 6.9822588e+08 (4798)
15444 Obj 5.53077e+10 Primal inf 2.7154194e+08 (3361)
15795 Obj 5.5339845e+10 Primal inf 4.1978686e+08 (3380)
16146 Obj 5.5622533e+10 Primal inf 3.5100471e+08 (3275)
16497 Obj 5.5624299e+10 Primal inf 3.4903668e+08 (3193)
16848 Obj 5.7925054e+10 Primal inf 5.1941856e+09 (6992)
17199 Obj 6.012461e+10 Primal inf 1.04555e+09 (4692)
17550 Obj 6.0306981e+10 Primal inf 1.1722589e+09 (5862)
17901 Obj 6.0862877e+10 Primal inf 1.198943e+09 (5963)
18252 Obj 6.132705e+10 Primal inf 2.7357742e+08 (3419)
18603 Obj 6.2754073e+10 Primal inf 1.0785967e+10 (15956)
18954 Obj 6.3096465e+10 Primal inf 9.0638195e+08 (5249)
19305 Obj 6.3575426e+10 Primal inf 2.3448293e+09 (7006)
19656 Obj 6.3615711e+10 Primal inf 5.1305132e+08 (5879)
20007 Obj 6.3851332e+10 Primal inf 7.3210072e+08 (4747)
20358 Obj 6.430071e+10 Primal inf 6.1131777e+08 (5798)
20709 Obj 6.4546662e+10 Primal inf 4.7115473e+08 (3590)
21060 Obj 6.4857737e+10 Primal inf 29170416 (1402)
21338 Obj 6.5074271e+10 Primal inf 3.6240782e+08 (3395)
21689 Obj 6.5460466e+10 Primal inf 28384239 (1432)
22040 Obj 6.5591942e+10 Primal inf 2.7061726e+08 (1356)
INFO:linopy.constants: Optimization successful:
Status: ok
Termination condition: optimal
Solution: 21906 primals, 50377 duals
Objective: 6.58e+10
Solver model: not available
Solver message: Optimal - objective value 65813180032.18475342
22391 Obj 6.5698207e+10 Primal inf 4690398.9 (574)
22742 Obj 6.5805975e+10 Primal inf 1013843.1 (147)
23003 Obj 6.5813274e+10
Optimal - objective value 6.581318e+10
After Postsolve, objective 6.581318e+10, infeasibilities - dual 0 (0), primal 0 (0)
Optimal objective 6.581318003e+10 - 23003 iterations time 5.672, Presolve 0.04
Total time (CPU seconds): 5.97 (Wallclock seconds): 5.97
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-ext-p-lower, Generator-ext-p-upper, StorageUnit-ext-p_dispatch-lower, StorageUnit-ext-p_dispatch-upper, StorageUnit-ext-p_store-lower, StorageUnit-ext-p_store-upper, StorageUnit-ext-state_of_charge-lower, StorageUnit-ext-state_of_charge-upper, StorageUnit-energy_balance were not assigned to the network.
Wind, solar and demand time series:
url = (
"https://tubcloud.tu-berlin.de/s/nwCrNLrtL6LAN3W/download/time-series-lecture-2.csv"
)
ts = pd.read_csv(url, index_col=0, parse_dates=True)
Power plants in Europe
url = (
"https://raw.githubusercontent.com/PyPSA/powerplantmatching/master/powerplants.csv"
)
ppl = pd.read_csv(url, index_col=0)
geometry = gpd.points_from_xy(ppl["lon"], ppl["lat"])
ppl = gpd.GeoDataFrame(ppl, geometry=geometry, crs=4326)
NUTS2 regions:
url = "https://tubcloud.tu-berlin.de/s/RHZJrN8Dnfn26nr/download/NUTS_RG_10M_2021_4326.geojson"
nuts = gpd.read_file(url).set_index("id").query("LEVL_CODE == 2")
An atlite cutout:
cutout = atlite.Cutout("PORTUGAL-2013-01-era5.nc")
Limitations of Static Plotting with Matplotlib#
You will agree that using matplotlib for static plotting is great for reports, but that it’s lacking some features for interactive visualisation.
ts["onwind [pu]"].plot(figsize=(10, 2))
<Axes: >
There are many Python-based interactive plotting libraries out there, and it can be confusing to keep an overview. This tutorial introduces you to two of them:
hvPlot, which is a high-level API mostly for bokeh plots that integrates nicely with
pandas.plotly.express, which is a high-level API for plotly plots.
These two tools allow you to produce shiny interactive figures with minimal code, however, at the expense of fewer customisation options.
hvPlot#
.hvplot() is a powerful and interactive Pandas-like .plot() API. You just replace .plot() with .hvplot() and you get an interactive figure. Simple as that.
It can be installed via conda or mamba in the following way:
conda install -c pyviz hvplot geoviews
Documentation can be found here: https://hvplot.holoviz.org/index.html
To use it, we have to import hvplot.pandas, which makes the .hvplot accessor available on Pandas DataFrame and Series objects, which means that after that df.hvplot becomes a valid statement while before that it would raise an error.
import hvplot.pandas
Let’s try it by plotting onshore wind time series for the year…
ts["onwind [pu]"].hvplot(height=200)
… or the load time series for February
ts.loc["2015-02", "load [GW]"].hvplot(height=200)
We can also plot geographic data with hvPlot, for instance, the locations of all hard coal power plants in Europe.
The geo=True declares that the data will be plotted in a geographic coordinate system.
Once hvPlot knows that your data is in geo-coordinates, you can use the tiles keyword argument to overlay a the plot on top of map tiles.
Note
For a list of available tiles, look here.
ppl.query("Fueltype == 'Hard Coal'").hvplot(
geo=True, tiles=True, frame_height=600, frame_width=600
)
Like in geopandas, we can tell hvPlot to plot the point sizes and colors according to columns of the pandas.DataFrame. We can also change the opacity with alpha and the colormap with cmap.
plot = ppl.query("Fueltype == 'Hard Coal'").hvplot(
geo=True,
tiles="CartoLight",
frame_height=600,
c="DateIn",
cmap="viridis",
s="Capacity",
alpha=0.6,
)
plot
There are a few more options of the graph we can tweak in the opts() section, like which tools should be activated by default.
plot = plot.opts(xaxis=None, yaxis=None, active_tools=["pan", "wheel_zoom"])
plot
All this does not only work with points but also shapes. We can also pick the columns that should be shown when hovering on a shape using hover_cols.
nuts.hvplot(
geo=True,
tiles="OSM",
hover_cols=["NUTS_NAME", "NUTS_ID"],
c="CNTR_CODE",
frame_height=500,
alpha=0.2,
).opts(xaxis=None, yaxis=None, active_tools=["pan", "wheel_zoom"])
We can also use hvPlot for xarray datasets (e.g. atlite cutouts).
For that, we need to import the corresponding xarray accessors.
import hvplot.xarray
So let’s try it by plotting the wind speeds in Portugal as provided by ERA5. The nice thing you will notice is that it will automatically open a panel for dimensions that we did not select explicitly. In this case we can easily sweep across the time dimension. Notice also the customisation options we use here.
cutout.data.hvplot.quadmesh(
"x",
"y",
"wnd100m",
frame_height=500,
cmap="Blues",
geo=True,
tiles="CartoLight",
alpha=0.8,
padding=0.5,
clim=(0, 10),
)
We can also plot the time series of solar generation in Germany on a heatmap:
ts.hvplot.heatmap(
x="index.hour", y="index.month", C="solar [pu]", cmap="blues"
).aggregate(function="mean")
hvPlot also offers stacked area charts that come in handy for plotting the power dispatch of a solved PyPSA network:
dispatch = (
pd.concat([n.generators_t.p, n.storage_units_t.p], axis=1).loc["2015-02"].div(1e3)
)
dispatch.where(dispatch > 0, 0).hvplot.area(
stacked=True,
line_width=0,
width=1300,
height=350,
hover=False,
color=[n.carriers.at[c, "color"] for c in dispatch.columns],
ylabel="electricity supply [GW]",
ylim=(0, 180),
)
hvPlot also has a nice explorer that can be displayed in a Jupyter notebook and that can be used to quickly create customized plots.
hvplot.explorer(pd.DataFrame(ppl))
Plotly Express#
The
plotly.expressmodule (usually imported as px) contains functions that can create entire figures at once. Plotly Express is a built-in part of theplotlylibrary, and is the recommended starting point for creating most common figures. Every Plotly Express function uses graph objects internally and returns a plotly.graph_objects.Figure instance. Throughout the plotly documentation, you will find the Plotly Express way of building figures at the top of any applicable page, followed by a section on how to use graph objects to build similar figures. Any figure created in a single function call with Plotly Express could be created using graph objects alone, but with between 5 and 100 times more code.
Documentation is available here: https://plotly.com/python/plotly-express/
It can be installed via conda or mamba in the following way:
conda install -c conda-forge plotly
import plotly.io as pio
import plotly.express as px
import plotly.offline as py
Note
We need to import plotly.io and plotly.offline, so that the interactive plots are also visible on the course’s static website.
Let’s reproduce the plots we previously created with hvPlot. Onshore wind capacity factor time series:
px.line(ts["onwind [pu]"])
Load time series in February:
px.line(ts.loc["2015-02", "load [GW]"])
Hard coal power plants in Europe:
df = ppl.query("Fueltype == 'Hard Coal'")
px.scatter_mapbox(
df, lat="lat", lon="lon", mapbox_style="carto-positron", zoom=2, height=600
)
px.scatter_mapbox(
df,
lat="lat",
lon="lon",
mapbox_style="carto-positron",
color="DateIn",
size="Capacity",
zoom=2,
height=600,
)
px.choropleth_mapbox(
nuts,
geojson=nuts.geometry,
locations=nuts.index,
mapbox_style="carto-positron",
zoom=2,
height=600,
color="CNTR_CODE",
center={"lat": 48, "lon": 12},
)